package mviewer.types;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.Set;

public class Model3d implements IModel3d, IModel3dBuilder {

	private String name;
	protected ArrayList<Vertex> vertices;
	protected ArrayList<Polygon3d> polygons;
	protected ArrayList<VectorData> normals;
	protected ArrayList<TextureCoordinate> textureCoordinates;
	private Material material = new Material();
	protected Transformation transformation = new Transformation();
	protected boolean textureCoordinatesDefined = false;
	protected boolean modified = false;

	public Model3d() {
		this("model");
	}

	public Model3d(String name) {
		this(name, 4000, 3000);
	}

	public Model3d(String name, int vertexCount, int polygonCount) {
		vertices = new ArrayList<Vertex>(vertexCount);
		polygons = new ArrayList<Polygon3d>(polygonCount);
		normals  = new ArrayList<VectorData>(vertexCount + polygonCount);
		textureCoordinates = new ArrayList<TextureCoordinate>(polygonCount * 3);
		setName(name);
	}

	@Override
	public final void setName(String name) {
		this.name = name;
	}

	@Override
	public String getName() {
		return name;
	}

	@Override
	public void draw(IDrawerContext drawerContext) {
		throwExceptionIfIllegalState();
		IDrawer drawer = getDrawer(drawerContext);

		drawer.pushTransformation(transformation);
		drawer.draw(new SegregatedDataIterator(name, polygons.iterator()), material);
		drawer.popTransformation();
	}

	@Override
	public Material getMaterial() {
		return material;
	}

	@Override
	public void setMaterial(Material material) {
		if (material == null)
			throw new IllegalArgumentException("material cannot be null");
		this.material = material;
	}

	@Override
	public Transformation getTransformation() {
		return transformation;
	}

	@Override
	public String toString() {
		return String.format("Model3d{vertexCount : %d, polygonCount : %d}",
							vertices.size(), polygons.size());
	}

	@Override
	public void apply() {
		vertices.trimToSize();
		polygons.trimToSize();
		normals.trimToSize();
		textureCoordinates.trimToSize();

		renderNormals();

		modified = false;
	}

	@Override
	public void appendVertex(float x, float y, float z) {
		modified = true;
		VectorData normal = new VectorData();
		Vertex vertex = new Vertex(x, y, z, normal);

		vertices.add(vertex);
		normals.add(normal);
	}

	@Override
	public void appendTextureCoordinate(float u, float v) {
		textureCoordinates.add(new TextureCoordinate(u, v));
	}

	@Override
	public void appendPolygon(int[] vertexIndices, int[] textureIndices) {
		if (vertexIndices.length < 3)
			throw new IllegalArgumentException("A polygon has to have at least three vertices.");
		for (int i = 1; i < vertexIndices.length - 1; i++)
			appendPolygonInternal(new int[] {vertexIndices[0],
									vertexIndices[i],
									vertexIndices[i + 1]},
								  new int[] {textureIndices[0],
									textureIndices[i],
									textureIndices[i + 1]});
	}

	@Override
	public void appendPolygon(int[] vertexIndices) {
		if (vertexIndices.length < 3)
			throw new IllegalArgumentException("A polygon has to have at least three vertices.");
		for (int i = 1; i < vertexIndices.length - 1; i++)
			appendPolygonInternal(new int[] {vertexIndices[0],
											 vertexIndices[i],
											 vertexIndices[i + 1]});
	}

	@Override
	public void listContainedMaterials(Set<Material> materials) {
		materials.add(material);
	}

	@Override
	public void listContainedModels(Collection<IModel3d> models) {
		models.add(this);
	}

	@Override
	public Iterator<IWritableVector> transformableVertexIterator() {
		return new SegregatedDataIterator<IWritableVector>(name, vertices.iterator());
	}

	@Override
	public Iterator<IWritableVector> transformableNormalIterator() {
		return new SegregatedDataIterator<IWritableVector>(name, normals.iterator());
	}

	@Override
	public IModel3d clone() {
		Model3d res = new Model3d(name + IModel3d.COPY_SUFFIX, vertices.size(), polygons.size());
		for (IVertex v : vertices)
			res.appendVertex(v.getX(), v.getY(), v.getZ());
		for (TextureCoordinate tc : textureCoordinates)
			res.appendTextureCoordinate(tc.getU(), tc.getV());
		for (Polygon3d p : polygons) {
			TexturedPolygon3d tp = (TexturedPolygon3d)p.getTexturedPolygon();
			res.setMaterial(p.getMaterial());
			if (tp == null)
				res.appendPolygon(p.getVertexIndices());
			else
				res.appendPolygon(tp.getVertexIndices(), tp.getTextureCoordinateIndices());
		}
		res.setMaterial(material);
		res.transformation = transformation;
		// copy already rendered normals
		for (int i = 0; i < normals.size(); i++) {
			res.normals.get(i).setX(normals.get(i).getX());
			res.normals.get(i).setY(normals.get(i).getY());
			res.normals.get(i).setZ(normals.get(i).getZ());
		}
		res.modified = false;
		return res;
	}

	protected void throwExceptionIfIllegalState() {
		if (modified)
			throw new IllegalStateException("Modifications have not been applied. Call apply() first.");
	}

	private IVertex[] retrieveVertices(int[] vertexIndices) {
		IVertex[] res = new IVertex[vertexIndices.length];
		for (int i = 0; i < vertexIndices.length; i++)
			res[i] = vertices.get(vertexIndices[i]);
		return res;
	}

	private TextureCoordinate[] retrieveTextureCoordinates(int[] tcIndices) {
		TextureCoordinate[] res = new TextureCoordinate[tcIndices.length];
		for (int i = 0; i < tcIndices.length; i++)
			res[i] = textureCoordinates.get(tcIndices[i]);
		return res;
	}

	private void appendPolygonInternal(int[] vertexIndices) {
		modified = true;
		VectorData normal = new VectorData();
		Polygon3d polygon = new Polygon3d(vertexIndices,
										 retrieveVertices(vertexIndices),
										 normal,
										 material);

		polygons.add(polygon);
		normals.add(normal);

		for (IVertex vertex : polygon.getVertices())
			((Vertex)vertex).getPolygons().add(polygon);
	}

	private void appendPolygonInternal(int[] vertexIndices,
									   int[] textureCoordinateIndices) {
		modified = true;
		textureCoordinatesDefined = true;
		VectorData normal = new VectorData();
		Polygon3d polygon = new TexturedPolygon3d(vertexIndices,
									retrieveVertices(vertexIndices),
									normal,
									material,
									textureCoordinateIndices,
									retrieveTextureCoordinates(textureCoordinateIndices));

		polygons.add(polygon);
		normals.add(normal);

		for (IVertex vertex : polygon.getVertices())
			((Vertex)vertex).getPolygons().add(polygon);
	}

	private void renderNormals() {
		for (Polygon3d p : polygons)
			renderPolygonNormal(p);

		for (Vertex v : vertices)
				renderVectorNormalAsAveragePolygonNormal(v);

		for (VectorData v : normals) {
			float l = VectorUtil.length(v);
			if ((l > 1.1 || l < 0.9) && l != 0)
				throw new UnsupportedOperationException("normal not normalized, length: " + l);
		}
	}

	private void renderVectorNormalAsAveragePolygonNormal(Vertex v) {
		VectorData normal = (VectorData)v.getNormal();
		if (normal.getX() != 0 && normal.getY() != 0 && normal.getZ() != 0)
			return;
		normal.setX(0);
		normal.setY(0);
		normal.setZ(0);
		Collection<Polygon3d> poly = v.getPolygons();
		int polyCount = poly.size();

		if (polyCount == 0)
		//	return res;
			throw new UnsupportedOperationException("lonely vertex");

		for (Polygon3d p : poly)
			VectorUtil.add(normal, p.getNormal(), normal);

		VectorUtil.divide(normal, polyCount, normal);
		VectorUtil.normalize(normal);
	}

	private void renderPolygonNormal(Polygon3d p) {
		// returns (B-A) x (C-A)
		// which leads us to normal equitation E: (X - A) scalar n = distance
		VectorData n = (VectorData)p.getNormal();
		IVertex[] v = p.getVertices();
		VectorData a = new VectorData(), b = new VectorData();
		VectorUtil.sub(v[1], v[0], a);
		VectorUtil.sub(v[2], v[0], b);
		VectorUtil.crossProduct(a, b, n);
		VectorUtil.normalize(n);
	}

	protected IDrawer getDrawer(IDrawerContext drawerContext) {
		int feature = IDrawerContext.POLYGONS_COLORED;
		if (material.getTextureFile() != null)
			feature = textureCoordinatesDefined
						? IDrawerContext.MODEL_TEXTURED
						: IDrawerContext.MODEL_TEXTURE_COORDINATES_GENERATED;
		return drawerContext.getDrawer(feature);
	}

	@Override
	public float calculateDiagonalLength() {
		float x, y, z, modelSize,
			lx = retrieveLowestX(),
			hx = retrieveHighestX(),
			ly = retrieveLowestY(),
			hy = retrieveHighestY(),
			lz = retrieveLowestZ(),
			hz = retrieveHighestZ();

		x = hx - lx;
		y = hy - ly;
		z = hz - lz;
		return VectorUtil.length(x, y, z);
	}

	@Override
	public IVector calculateCenter() {
		float lx = retrieveLowestX(), ly = retrieveLowestY(), lz = retrieveLowestZ();
		return new VectorData(lx + (retrieveHighestX() - lx) / 2,
							ly + (retrieveHighestY() - ly) / 2,
							lz + (retrieveHighestZ() - lz) / 2);
	}

	@Override
	public float retrieveLowestX() {
		float res = Float.MAX_VALUE;
		for (VectorData v : vertices)
			if (v.getX() < res)
				res = v.getX();
		return res;
	}

	@Override
	public float retrieveHighestX() {
		float res = Float.MIN_VALUE;
		for (VectorData v : vertices)
			if (v.getX() > res)
				res = v.getX();
		return res;
	}

	@Override
	public float retrieveLowestY() {
		float res = Float.MAX_VALUE;
		for (VectorData v : vertices)
			if (v.getY() < res)
				res = v.getY();
		return res;
	}

	@Override
	public float retrieveHighestY() {
		float res = Float.MIN_VALUE;
		for (VectorData v : vertices)
			if (v.getY() > res)
				res = v.getY();
		return res;
	}

	@Override
	public float retrieveLowestZ() {
		float res = Float.MAX_VALUE;
		for (VectorData v : vertices)
			if (v.getZ() < res)
				res = v.getZ();
		return res;
	}

	@Override
	public float retrieveHighestZ() {
		float res = Float.MIN_VALUE;
		for (VectorData v : vertices)
			if (v.getZ() > res)
				res = v.getZ();
		return res;
	}
}